/**
 * Fetch https://developer.mozilla.org/zh-CN/docs/Web/API/fetch
 */
export type ResponseType =
    | 'arrayBuffer'
    | 'blob'
    | 'document'
    | 'json'
    | 'text'
    | 'stream'

// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/TRACE
export type Method =
    | 'GET'
    | 'DELETE'
    | 'HEAD'
    | 'OPTIONS'
    | 'POST'
    | 'PUT'
    | 'PATCH'
    | 'PURGE'
    | 'LINK'
    | 'UNLINK'

export interface FetchOptions<P = any, D = any> extends RequestInit {
    url?: string;
    method?: Method;
    params?: P;
    data?: D /* | BodyInit */;
    responseType?: ResponseType;
    timeout?: number;
    headers?: Headers
    // [propName: string]: any;
}
interface FetchResponseOptions {
    url: string;
}
export interface FetchResponse {
    status: number;
    statusText: string;
    data: any;
    options: FetchResponseOptions;
}

const buildURL = (base: string, url: string): string => {
    if (!url || !base || /^http(s?):\/\//i.test(url)) {
        return url || ''
    }
    return `${base.replace(/\/$/, '')}/${url.replace(/^\//, '')}`
}

const textTypes = new Set([
    'image/svg',
    'application/xml',
    'application/xhtml',
    'application/html',
])

const jsonReg = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i

function detectResponseType(cType: string | null): ResponseType {
    if (!cType) {
        return 'json'
    }

    // Value might look like: `application/json; charset=utf-8`
    const contentType = cType.split(';').shift() || ''

    if (jsonReg.test(contentType)) {
        return 'json'
    }

    // TODO
    // if (contentType === 'application/octet-stream') {
    //   return 'stream'
    // }

    if (textTypes.has(contentType) || contentType.startsWith('text/')) {
        return 'text'
    }

    return 'blob'
}

type InterceptorRequestHandler = (options: FetchOptions) => Promise<FetchOptions>
type InterceptorResponseHandler = (options: FetchResponse) => Promise<any>
class Interceptors {
    private requestHandlers: InterceptorRequestHandler[] = []
    async requestEach(config: FetchOptions) {
        let conf: any = config
        for (const handler of this.requestHandlers) {
            if (handler) {
                conf = await handler(conf)
            }
        }
        return conf
    }
    request(...args: any[]) {
        this.requestHandlers.push(...args)
    }
    private responseHandlers: InterceptorResponseHandler[] = []
    async responseEach(response: FetchResponse) {
        let respond: any = response
        for (const handler of this.responseHandlers) {
            if (handler) {
                respond = await handler(respond)
            }
        }
        return respond
    }
    response(...args: any[]) {
        this.responseHandlers.push(...args)
    }
}

export type CredentialsType = 'include' | 'omit' | 'same-origin' | undefined

class IFetch {
    private baseURL = ''
    private timeout = 10 * 1000
    private credentials: CredentialsType
    interceptors = new Interceptors()
    constructor(
        baseURL?: string,
        timeout?: number | null,
        credentials?: CredentialsType
    ) {
        if (baseURL) {
            this.baseURL = baseURL
        }
        if (timeout) {
            this.timeout = timeout
        }
        if (credentials) {
            this.credentials = credentials
        }
    }
    async request<P, D>(options: FetchOptions<P, D>) {
        if (!options.url) {
            throw new Error('options.url not define!')
        }
        const {
            url: _url,
            method,
            params,
            data,
            responseType,
            timeout,
            headers,
            ...rest
        } = options
        let url: string = buildURL(this.baseURL, _url)
        const queries: {
            [propName: string]: any
        } = params || {}
        if (queries && typeof queries === 'object') {
            const [start, end] = url.split('?')
            const search = new URLSearchParams(end)
            Object.keys(queries).forEach(key => search.append(key, queries[key]))
            url = `${start}?${search.toString()}`
        }
        const abortInstance = new AbortController()
        const fetchOptions: FetchOptions = {
            ...rest,
            method,
            signal: abortInstance.signal,
            headers: new Headers(headers || {})
        }
        if (this.credentials) {
            fetchOptions.credentials = this.credentials
        }
        const postData = data
        if (postData && method && !['GET', 'HEAD'].includes(method)) {
            const dataType: string = Object.prototype.toString.call(postData)
            if (
                [
                    '[object FormData]',
                    '[object Blob]',
                    '[object File]',
                ].includes(dataType)
            ) {
                fetchOptions.body = postData as BodyInit
            } else {
                fetchOptions.body = typeof postData === 'string'
                    ? postData
                    : JSON.stringify(postData) as string
            }
        }
        // console.log('fetchOptions.body', fetchOptions.body)
        const config = await this.interceptors.requestEach(fetchOptions)
        const timer = setTimeout(() => abortInstance.abort(), timeout || this.timeout)
        const response: Response = await fetch(url, config)
        clearTimeout(timer)
        const respondType = responseType || detectResponseType(
            response.headers.get('Content-type')
        )
        const back = async (respondType: ResponseType, response: Response) => {
            switch (respondType) {
                case 'text':
                    return await response.text()
                case 'blob':
                    return await response.blob()
                case 'arrayBuffer':
                    return await response.arrayBuffer()
                case 'stream':
                    return response.body
                case 'json':
                default:
                    return await response.json()
            }
        }
        const { status, statusText } = response
        const responseData = await back(respondType, response)
        const respond: FetchResponse = {
            status,
            statusText,
            data: responseData,
            options: {
                url: response.url
            }
        }
        const result = await this.interceptors.responseEach(respond)
        // console.log('result', result)
        if (status >= 200 && status < 400) {
            return result
        } else {
            throw result
        }
    }
    get(url: string, options?: FetchOptions) {
        return this.request({ ...options, url, method: 'GET' })
    }
    post(url: string, options?: FetchOptions) {
        return this.request({ ...options, url, method: 'POST' })
    }
    put(url: string, options?: FetchOptions) {
        return this.request({ ...options, url, method: 'PUT' })
    }
    delete(url: string, options?: FetchOptions) {
        return this.request({ ...options, url, method: 'DELETE' })
    }
}

export default IFetch
